<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<style>

canvas { border: 1px solid black; margin: 5px; }


</style>
</head>
<body>

<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<div>glsl</div>
<canvas id="glsl"></canvas>
<div>js</div>
<canvas id="js"></canvas>


</body>
<script>

const v3 = twgl.v3;

// note: I'm using twgl to make the code smaller.
// This is not lesson in WebGL. You should already know what it means
// to setup buffers and attributes and set uniforms and create textures.
// What's important is the technique, not the minutia of WebGL. If you
// don't know how to do those things you need a much bigger tutorial
// on WebGL like https://webglfundamentals.org

function main() {
  const gl = document.createElement('canvas').getContext('webgl');
  const ext = gl.getExtension('OES_texture_float');
  if (!ext) {
    alert('need OES_texture_float');
    return;
  }
  
  const r = max => Math.random() * max;
  const hsl = (h, s, l) => `hsl(${h * 360},${s * 100 | 0}%,${l * 100 | 0}%)`;
  function createPoints(numPoints) {
    const points = [];
    for (let i = 0; i < numPoints; ++i) {
      points.push(r(300), r(150), 0, 0);  // RGBA
    }
    return points;
  }

  function distanceFromPointToLineSquared(a, b, c) {
    const ba = v3.subtract(a, b);
    const bc = v3.subtract(c, b);
    const dot = v3.dot(ba, bc);
    const lenSq = v3.lengthSq(bc);
    let param = 0;
    if (lenSq !== 0) {
      param = Math.min(1, Math.max(0, dot / lenSq));
    }
    const r = v3.add(b, v3.mulScalar(bc, param));
    return v3.distanceSq(a, r);
  }

  const aPoints = createPoints(6);
  const bPoints = createPoints(15);
  const cPoints = createPoints(15);
  
  // do it in JS to check
  {
    // compute closest lines to points
    const closest = [];
    for (let i = 0; i < aPoints.length; i += 4) {
      const a = aPoints.slice(i, i + 3);
      let minDistSq = Number.MAX_VALUE;
      let minIndex = -1;
      for (let j = 0; j < bPoints.length; j += 4) {
        const b = bPoints.slice(j, j + 3);
        const c = cPoints.slice(j, j + 3);
        const distSq = distanceFromPointToLineSquared(a, b, c);
        if (distSq < minDistSq) {
          minDistSq = distSq;
          minIndex = j / 4;
        }
      }
      closest.push(minIndex);
    }

    drawResults(document.querySelector('#js'), closest);
  }

  const vs = `
  attribute vec4 position;
  void main() {
    gl_Position = position;
  }
  `;
  
  const fs = `
  precision highp float;

  uniform sampler2D aValues;
  uniform vec2 aDimensions;  // the size of the aValues texture in pixels (texels)
  uniform sampler2D bValues;
  uniform vec2 bDimensions;  // the size of the bValues texture in pixels (texels)
  uniform sampler2D cValues;
  uniform vec2 cDimensions;  // the size of the cValues texture in pixels (texels)
  uniform vec2 outputDimensions; // the size of the thing we're drawing to (canvas)

  // this code, given a sampler2D, the size of the texture, and an index
  // computes a UV coordinate to pull one RGBA value out of a texture
  // as though the texture was a 1D array.
  vec3 getPoint(in sampler2D tex, in vec2 dimensions, in float index) {
    vec2 uv = (vec2(
       floor(mod(index, dimensions.x)),
       floor(index / dimensions.x)) + 0.5) / dimensions;
    return texture2D(tex, uv).xyz;
  }

  // from https://stackoverflow.com/a/6853926/128511
  float distanceFromPointToLine(in vec3 a, in vec3 b, in vec3 c) {
    vec3 ba = a - b;
    vec3 bc = c - b;
    float d = dot(ba, bc);
    float len = length(bc);
    float param = 0.0;
    if (len != 0.0) {
      param = clamp(d / (len * len), 0.0, 1.0);
    }
    vec3 r = b + bc * param;
    return distance(a, r);
  }

  void main() {
    // gl_FragCoord is the coordinate of the pixel that is being set by the fragment shader.
    // It is the center of the pixel so the bottom left corner pixel will be (0.5, 0.5).
    // the pixel to the left of that is (1.5, 0.5), The pixel above that is (0.5, 1.5), etc...
    // so we can compute back into a linear index 
    float ndx = floor(gl_FragCoord.y) * outputDimensions.x + floor(gl_FragCoord.x); 
    
    // find the closest points
    float minDist = 10000000.0; 
    float minIndex = -1.0;
    vec3 a = getPoint(aValues, aDimensions, ndx);
    for (int i = 0; i < ${bPoints.length / 4}; ++i) {
      vec3 b = getPoint(bValues, bDimensions, float(i));
      vec3 c = getPoint(cValues, cDimensions, float(i));
      float dist = distanceFromPointToLine(a, b, c);
      if (dist < minDist) {
        minDist = dist;
        minIndex = float(i);
      }
    }
    
    // convert to 8bit color. The canvas defaults to RGBA 8bits per channel
    // so take our integer index (minIndex) and convert to float values that
    // will end up as the same 32bit index when read via readPixels as
    // 32bit values.
    gl_FragColor = vec4(
      mod(minIndex, 256.0),
      mod(floor(minIndex / 256.0), 256.0),
      mod(floor(minIndex / (256.0 * 256.0)), 256.0) ,
      floor(minIndex / (256.0 * 256.0 * 256.0))) / 255.0;
  }
  `;
  
  // compile shader, link program, lookup locations
  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
  
  // calls gl.createBuffer, gl.bindBuffer, gl.bufferData for a -1 to +1 quad
  const bufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);

  // make an RGBA float texture for each set of points
  // calls gl.createTexture, gl.bindTexture, gl.texImage2D, gl.texParameteri
  const aTex = twgl.createTexture(gl, {
    src: aPoints,
    width: aPoints.length / 4,
    type: gl.FLOAT,
    minMag: gl.NEAREST,
  });
  const bTex = twgl.createTexture(gl, {
    src: bPoints,
    width: bPoints.length / 4,
    type: gl.FLOAT,
    minMag: gl.NEAREST,
  });
  const cTex = twgl.createTexture(gl, {
    src: cPoints,
    width: cPoints.length / 4,
    type: gl.FLOAT,
    minMag: gl.NEAREST,
  });
    
  const numOutputs = aPoints.length / 4;
  gl.canvas.width = numOutputs;
  gl.canvas.height = 1;
  gl.viewport(0, 0, numOutputs, 1);
  
  gl.useProgram(programInfo.program);  
  
  // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  
  // calls gl.activeTexture, gl.bindTexture, gl.uniform
  twgl.setUniforms(programInfo, {
    aValues: aTex,
    aDimensions: [aPoints.length / 4, 1],
    bValues: cTex,
    bDimensions: [bPoints.length / 4, 1],
    cValues: bTex,
    cDimensions: [cPoints.length / 4, 1],
    outputDimensions: [aPoints.length / 4, 1],
  });
  
  // draw the quad
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
  
  // get result
  const pixels = new Uint8Array(numOutputs * 4);
  const results = new Uint32Array(pixels.buffer);
  gl.readPixels(0, 0, numOutputs, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
  drawResults(document.querySelector('#glsl'), results);


  function drawResults(canvas, closest) {
    const ctx = canvas.getContext('2d');
    
    // draw the lines
    ctx.beginPath();
    for (let j = 0; j < bPoints.length; j += 4) {
      const b = bPoints.slice(j, j + 2);
      const c = cPoints.slice(j, j + 2);
      ctx.moveTo(...b);
      ctx.lineTo(...c);
    }
    ctx.strokeStyle = '#888';
    ctx.stroke();
    
    // draw the points and closest lines
    for (let i = 0; i < aPoints.length; i += 4) {
      const a = aPoints.slice(i, i + 2);
      const ndx = closest[i / 4] * 4;
      const b = bPoints.slice(ndx, ndx + 2);
      const c = cPoints.slice(ndx, ndx + 2);
      const color = hsl(i / aPoints.length, 1, 0.4);
      ctx.fillStyle = color;
      ctx.strokeStyle = color;
      ctx.fillRect(a[0] - 2, a[1] - 2, 5, 5);
      ctx.beginPath();
      ctx.moveTo(...b);
      ctx.lineTo(...c);
      ctx.stroke();
    }
  }

}
main();


</script>
